// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.


#include <jni.h>
#include "core/cheats/cheat_base.h"
#include "core/cheats/cheats.h"
#include "core/cheats/gateway_cheat.h"
#include "core/core.h"
#include "core/hle/service/cfg/cfg.h"
#include "mem_region.h"
#include "network/network.h"
#include "video_core/renderer_base.h"
#include "aiwu.h"
#include "input_manager.h"

static std::string GetJString(JNIEnv *env, jstring jstr) {
    if (!jstr) {
        return {};
    }

    const char *s = env->GetStringUTFChars(jstr, nullptr);
    std::string result = s;
    env->ReleaseStringUTFChars(jstr, s);
    return result;
}

static jobject ToJavaNetworkError(Network::RoomMember::Error error) {
    static const std::map<Network::RoomMember::Error, const char *> NetworkErrorNameMap{
            {Network::RoomMember::Error::LostConnection,     "LostConnection"},
            {Network::RoomMember::Error::HostKicked,         "HostKicked"},
            {Network::RoomMember::Error::UnknownError,       "UnknownError"},
            {Network::RoomMember::Error::NameCollision,      "NameCollision"},
            {Network::RoomMember::Error::MacCollision,       "MacCollision"},
            {Network::RoomMember::Error::ConsoleIdCollision, "ConsoleIdCollision"},
            {Network::RoomMember::Error::WrongVersion,       "WrongVersion"},
            {Network::RoomMember::Error::WrongPassword,      "WrongPassword"},
            {Network::RoomMember::Error::CouldNotConnect,    "CouldNotConnect"},
            {Network::RoomMember::Error::RoomIsFull,         "RoomIsFull"},
            {Network::RoomMember::Error::HostBanned,         "HostBanned"},
            {Network::RoomMember::Error::PermissionDenied,   "PermissionDenied"},
            {Network::RoomMember::Error::NoSuchUser,         "NoSuchUser"},
    };

    const auto name = NetworkErrorNameMap.count(error) ? NetworkErrorNameMap.at(error)
                                                       : "UnknownError";

    JNIEnv *env = IDCache::GetEnvForThread();
    const jclass net_play_error_class = IDCache::GetNetPlayErrorClass();
    return env->GetStaticObjectField(
            net_play_error_class, env->GetStaticFieldID(net_play_error_class, name,
                                                        "Lorg/citra/citra_emu/NativeLibrary$NetPlayError;"));
}

MultiplayerState::MultiplayerState() {
    if (auto member = Network::GetRoomMember().lock()) {
        // register the network structs to use in slots and signals
        state_callback_handle = member->BindOnStateChanged(
                [this](const Network::RoomMember::State &state) {
                    MultiplayerState::OnNetworkStateChanged(state);
                });
        error_callback_handle = member->BindOnError(
                [this](const Network::RoomMember::Error &error) {
                    MultiplayerState::OnNetworkError(error);
                });
        member->BindOnStatusMessageReceived(
                [this](const Network::StatusMessageEntry &status_message) {
                    MultiplayerState::OnStatusMessageReceive(status_message);
                });
    }
//    announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>();
//    announce_multiplayer_session->BindErrorCallback(
//            [this](const Common::WebResult &result) {
//                MultiplayerState::OnAnnounceFailed(result);
//            });
}

MultiplayerState::~MultiplayerState() {
    if (state_callback_handle) {
        if (auto member = Network::GetRoomMember().lock()) {
            member->Unbind(state_callback_handle);
        }
    }

    if (error_callback_handle) {
        if (auto member = Network::GetRoomMember().lock()) {
            member->Unbind(error_callback_handle);
        }
    }
}

void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State &state) {
    LOG_INFO(Frontend, "Network State: {}", Network::GetStateStr(state));
    if (state == Network::RoomMember::State::Joined) {
        JNIEnv *env = IDCache::GetEnvForThread();
        env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
                                  IDCache::GetNetPlayStateChanged(),
                                  static_cast<bool >(true), nullptr);
    }
}

void MultiplayerState::OnNetworkError(const Network::RoomMember::Error &error) {
    LOG_INFO(Frontend, "Network Error: {}", Network::GetErrorStr(error));
    JNIEnv *env = IDCache::GetEnvForThread();
    env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
                              IDCache::GetNetPlayStateChanged(),
                              static_cast<bool >(false), ToJavaNetworkError(error));
}

//void MultiplayerState::OnAnnounceFailed(const Common::WebResult &result) {
//    LOG_INFO(Frontend, "Network AnnounceFailed:");
//    announce_multiplayer_session->Stop();
//}
void MultiplayerState::OnStatusMessageReceive(const Network::StatusMessageEntry &status_message) {
    std::string name;
    if (status_message.username.empty() || status_message.username == status_message.nickname) {
        name.assign(status_message.nickname);
    } else {
        name.append(status_message.nickname)
                .append(" (")
                .append(status_message.username)
                .append(")");
    }
    std::string message;
    message.assign(name);
    switch (status_message.type) {
        case Network::IdMemberJoin:
            message.append(" 已进入房间");
            break;
        case Network::IdMemberLeave:
            message.append(" 已离开房间");
            break;
        case Network::IdMemberKicked:
            message.append(" 已被踢出房间");
            break;
        case Network::IdMemberBanned:
            message.append(" 已被封禁");
            break;
        case Network::IdAddressUnbanned:
            message.append(" 已被解封");
            break;
    }
    LOG_INFO(Frontend, "OnStatusMessageReceive: {}", message);
    JNIEnv *env = IDCache::GetEnvForThread();
    env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
                              IDCache::GetNetPlayStateMessageReceive(),
                              env->NewStringUTF(message.c_str()));
}

namespace AiWu {
    static std::shared_ptr<MultiplayerState> multiplayer_state;

    void InitState() {
        multiplayer_state = std::make_shared<MultiplayerState>();
    }

    bool NetPlayLeaveRoom() {
        if (auto room = Network::GetRoom().lock()) {
            // if you are in a room, leave it
            if (auto member = Network::GetRoomMember().lock()) {
                member->Leave();
                LOG_INFO(Frontend, "Left the room (as a client)");
            }

            // if you are hosting a room, also stop hosting
            if (room->GetState() != Network::Room::State::Open) {
                return true;
            }
            room->Destroy();
            LOG_INFO(Frontend, "Closed the room (as a server)");
        }
        return true;
    }
}

#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jboolean JNICALL
Java_org_citra_citra_1emu_NativeLibrary_AddCheat(JNIEnv *env, jclass clazz, jstring jDesc,
                                                 jstring jCode, jboolean jAddToEnd) {
    const std::string desc = GetJString(env, jDesc);
    const std::string code = GetJString(env, jCode);

//    auto code_lines = code.split('\n', QString::SkipEmptyParts);
//    for (int i = 0; i < code_lines.size(); ++i) {
//        Cheats::GatewayCheat::CheatLine cheat_line(code_lines[i].toStdString());
//        if (cheat_line.valid)
//            continue;
//    }
    auto cheat = std::make_shared<Cheats::GatewayCheat>(desc, code, code);

    Core::System::GetInstance().CheatEngine().AddCheat(cheat, jAddToEnd);
    //Core::System::GetInstance().CheatEngine().SaveCheatFile();
    return static_cast<jboolean>(true);
}
JNIEXPORT jboolean JNICALL
Java_org_citra_citra_1emu_NativeLibrary_UpdateCheat(JNIEnv *env, jclass clazz, jint jPostion,
                                                    jstring jDesc, jstring jCode) {
    const std::string desc = GetJString(env, jDesc);
    const std::string code = GetJString(env, jCode);

//    auto code_lines = code.split('\n', QString::SkipEmptyParts);
//    for (int i = 0; i < code_lines.size(); ++i) {
//        Cheats::GatewayCheat::CheatLine cheat_line(code_lines[i].toStdString());
//        if (cheat_line.valid)
//            continue;
//    }
    auto cheat = std::make_shared<Cheats::GatewayCheat>(desc, code, code);

    Core::System::GetInstance().CheatEngine().UpdateCheat(jPostion, cheat);
    return static_cast<jboolean>(true);
}
JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_NativeLibrary_SetCheatEnabled(JNIEnv *env, jclass clazz, jint jPostion,
                                                        jboolean jEnable) {
    std::vector<std::shared_ptr<Cheats::CheatBase>> cheats = Core::System::GetInstance().CheatEngine().GetCheats();
    cheats[jPostion]->SetEnabled(jEnable);
    LOG_INFO(Frontend, cheats[jPostion]->GetName().c_str());
}
JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_NativeLibrary_DeleteCheat(JNIEnv *env, jclass clazz, jint jPostion) {
    Core::System::GetInstance().CheatEngine().RemoveCheat(jPostion);
}
JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_NativeLibrary_SwitchBottomScreen(JNIEnv *env, jclass clazz,
                                                           jboolean jEnable) {
    VideoCore::g_renderer->SwitchBottomScreen(jEnable);
}

JNIEXPORT jintArray JNICALL
Java_org_citra_citra_1emu_NativeLibrary_loadPageTable(JNIEnv *env, jclass obj) {
    u32 start_addr = 0;
    u32 mem_size = 0;
    std::vector<u32> pages;
    Core::System &system{Core::System::GetInstance()};
    auto pagetable = system.Memory().GetCurrentPageTable();

    for (u32 i = 0; i < pagetable->GetPointerArray().size(); ++i) {
        auto p = pagetable->pointers[i];
        if (p != nullptr) {
            u32 addr = i << Memory::PAGE_BITS;
            if ((start_addr + mem_size) != addr) {
                if (mem_size > 0) {
                    pages.push_back(start_addr);
                    pages.push_back(mem_size);
                    mem_size = 0;
                }
                start_addr = addr;
            }
            mem_size += Memory::PAGE_SIZE;
        }
    }

    jintArray array = env->NewIntArray(pages.size());
    env->SetIntArrayRegion(array, 0, pages.size(), reinterpret_cast<const jint *>(pages.data()));
    return array;
}

JNIEXPORT jbyteArray JNICALL
Java_org_citra_citra_1emu_NativeLibrary_loadPage(JNIEnv *env, jclass obj, jint index) {
    Core::System &system{Core::System::GetInstance()};
    auto p = system.Memory().GetCurrentPageTable()->GetPointerArray()[index];
    if (p != nullptr) {
        jbyteArray array = env->NewByteArray(Memory::PAGE_SIZE);
        env->SetByteArrayRegion(array, 0, Memory::PAGE_SIZE, reinterpret_cast<const jbyte *>(p));
        return array;
    } else {
        return nullptr;
    }
}

JNIEXPORT jint JNICALL
Java_org_citra_citra_1emu_NativeLibrary_readMemory(JNIEnv *env, jclass obj, jint jAddr,
                                                   jint valueType) {
    u32 addr = static_cast<u32>(jAddr);
    u32 index = addr >> Memory::PAGE_BITS;
    u32 offset = addr & Memory::PAGE_MASK;
    Core::System &system{Core::System::GetInstance()};
    auto p = system.Memory().GetCurrentPageTable()->pointers[index];
    if (p != nullptr) {
        if (valueType == 0) {
            return *reinterpret_cast<u32 *>(p + offset);
        } else if (valueType == 1) {
            return *reinterpret_cast<u16 *>(p + offset);
        } else if (valueType == 2) {
            return *reinterpret_cast<u8 *>(p + offset);
        }
    }
    return 0;
}

JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_NativeLibrary_writeMemory(JNIEnv *env, jclass obj, jint jAddr,
                                                    jint valueType, jint value) {
    u32 addr = static_cast<u32>(jAddr);
    u32 index = addr >> Memory::PAGE_BITS;
    u32 offset = addr & Memory::PAGE_MASK;
    Core::System &system{Core::System::GetInstance()};
    auto p = system.Memory().GetCurrentPageTable()->pointers[index];
    if (p != nullptr) {
        if (valueType == 0) {
            *reinterpret_cast<u32 *>(p + offset) = static_cast<u32>(value);
        } else if (valueType == 1) {
            *reinterpret_cast<u16 *>(p + offset) = static_cast<u16>(value);
        } else if (valueType == 2) {
            *reinterpret_cast<u8 *>(p + offset) = static_cast<u8>(value);
        }
    }
}
JNIEXPORT jintArray JNICALL
Java_org_citra_citra_1emu_NativeLibrary_searchMemory(JNIEnv *env, jclass obj,
                                                     jint start_addr,
                                                     jint stop_addr,
                                                     jint value_type,
                                                     jint search_type,
                                                     jint scan_type,
                                                     jint value) {
    return searchMemoryRegion(start_addr, stop_addr, value_type, search_type, scan_type, value);
}
JNIEXPORT jintArray JNICALL
Java_org_citra_citra_1emu_NativeLibrary_getSearchResults(JNIEnv *env, jclass obj) {
    return getSearchResults();
}

JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_NativeLibrary_resetSearchResults(JNIEnv *env, jclass obj) {
    resetSearchResults();
}

JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_Screenshot(JNIEnv *env, jclass obj,
                                                                          jobject listener) {
    if (VideoCore::g_android_screenshot_complete_callback) {
        return;
    }
    jobject listener_ref = env->NewGlobalRef(listener);
    VideoCore::g_android_screenshot_complete_callback = [listener_ref](u32 width, u32 height,
                                                                       const std::vector<u32> &pixels) {
        JNIEnv *env = IDCache::GetEnvForThread();
        jclass listenerClass = env->GetObjectClass(listener_ref);
        jmethodID method = env->GetMethodID(listenerClass, "OnScreenshotComplete", "(II[I)V");
        jintArray array = ToJIntArray(pixels.data(), pixels.size());
        env->CallVoidMethod(listener_ref, method, (jint) width, (jint) height, array);
        env->DeleteLocalRef(array);
        env->DeleteGlobalRef(listener_ref);
    };
}

JNIEXPORT jboolean JNICALL
Java_org_citra_citra_1emu_NativeLibrary_NetPlayCreateRoom(JNIEnv *env, jclass obj,
                                                          jstring jUserName,
                                                          jstring jRoomName) {
    if (auto member = Network::GetRoomMember().lock()) {
        if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
            LOG_INFO(Frontend, "you are already joining");
            return static_cast<jboolean>(false);
        }
        const std::string room_name = GetJString(env, jRoomName);
        Core::System &system{Core::System::GetInstance()};
        std::string game_name;
        system.GetAppLoader().ReadTitle(game_name);
        u64 game_id;
        system.GetAppLoader().ReadProgramId(game_id);
        auto port = Network::DefaultRoomPort;
        auto password = "";
        auto max_connections = 16;
        Network::Room::BanList ban_list{};
        if (auto room = Network::GetRoom().lock()) {
            bool created = room->Create(room_name, "", "", port,
                                        password, max_connections,
                                        Settings::values.citra_username, game_name,
                                        game_id,
                                        std::make_unique<Network::VerifyUser::NullBackend>(),
                                        ban_list);
            if (!created) {
                LOG_INFO(Network, "Could not create room!");
                return static_cast<jboolean>(false);
            }
        }
        const std::string user_name = GetJString(env, jUserName);
        std::string token;
        member->Join(user_name,
                     Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), "127.0.0.1", port,
                     0, Network::NoPreferredMac, password, token);

//        Settings::Apply();
        return static_cast<jboolean>(member->IsConnected());
    }
    return static_cast<jboolean>(false);
}

JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_NativeLibrary_NetPlayLeaveRoom(JNIEnv *env, jclass obj) {
    AiWu::NetPlayLeaveRoom();
}

JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_NativeLibrary_NetPlayConnectToRoom(JNIEnv *env, jclass obj,
                                                             jstring jUserName,
                                                             jstring jServerAddr) {
    if (const auto member = Network::GetRoomMember().lock()) {
        // Prevent the user from trying to join a room while they are already joining.
        if (member->GetState() == Network::RoomMember::State::Joining) {
            LOG_INFO(Frontend, "you are already joining");
            return;
        } else if (member->IsConnected()) {
            if (!AiWu::NetPlayLeaveRoom()) {
                return;
            }
        }
    }
//    Settings::Apply();
    const std::string user_name = GetJString(env, jUserName);
    const std::string server_addr = GetJString(env, jServerAddr);

    if (auto room_member = Network::GetRoomMember().lock()) {
        auto port = Network::DefaultRoomPort;
        room_member->Join(user_name,
                          Service::CFG::GetConsoleIdHash(Core::System::GetInstance()),
                          server_addr.c_str(), port, 0,
                          Network::NoPreferredMac, "");
    }
}

JNIEXPORT jboolean JNICALL
Java_org_citra_citra_1emu_NativeLibrary_NetPlayIsHostedRoom(JNIEnv *env, jclass obj) {
    if (auto room = Network::GetRoom().lock()) {
        return static_cast<jboolean>(room->GetState() == Network::Room::State::Open);
    }
    return static_cast<jboolean>(false);
}

JNIEXPORT jboolean JNICALL
Java_org_citra_citra_1emu_NativeLibrary_NetPlayIsConnectedRoom(JNIEnv *env, jclass obj) {
    if (auto member = Network::GetRoomMember().lock()) {
        return static_cast<jboolean>((member->IsConnected()));
    }
    return static_cast<jboolean>(false);
}
JNIEXPORT jboolean JNICALL
Java_org_citra_citra_1emu_NativeLibrary_AiWuOnGamePadMoveEvent(JNIEnv *env, jclass obj,
                                                               jstring j_device, jint axis,
                                                               jfloat value, jboolean is_x) {
    if (is_x) {
        value = std::clamp(value, -1.f, 1.f);
    } else {
        value = std::clamp(-value, -1.f, 1.f);
    }
    float r = value * value * 2;
    if (r > 1.0f) {
        r = std::sqrt(r);
        value /= r;
    }
    return static_cast<jboolean>(InputManager::AnalogHandler()->AiWuMoveJoystick(axis, value,
                                                                                 is_x));
}
#ifdef __cplusplus
}
#endif
